package lol.clann.minecraft.springboot.plugin.bukkit.command;

import com.ruoshui.utils.java.exception.ExceptionUtils;
import com.ruoshui.utils.java.exception.UnreachableException;
import lol.clann.minecraft.springboot.api.annotation.command.*;
import lol.clann.minecraft.springboot.api.bukkit.command.TabComplete;
import lol.clann.minecraft.springboot.api.bukkit.command.constant.CommandConcurrentLevelEnum;
import lol.clann.minecraft.springboot.api.bukkit.command.resolver.CommandResolver;
import lol.clann.minecraft.springboot.api.bukkit.rawmessage.RawMessage;
import lol.clann.minecraft.springboot.api.bukkit.utils.PluginUtils;
import lol.clann.minecraft.springboot.api.context.SpringContext;
import lol.clann.minecraft.springboot.api.model.CacheItem;
import lol.clann.minecraft.springboot.api.service.CacheService;
import lol.clann.minecraft.springboot.plugin.bukkit.command.parameter.FlagParameter;
import lol.clann.minecraft.springboot.plugin.bukkit.command.parameter.NormalParameter;
import lol.clann.minecraft.springboot.plugin.bukkit.command.resolver.CommandResolvers;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandException;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 *
 * @author pyz
 * @date 2019/5/6 9:21 PM
 */
@ToString
@Getter
public class CommandExecutor {
    private final Object target;
    private final Method invoker;
    private final List<String> permissions = new ArrayList<>();
    private final String name;
    private final String showArgs;
    private final String description;
    private final List<Integer> senders = new ArrayList<>();
    private final List<Integer> flagMap = new ArrayList<>();
    private final List<FlagParameter> flags = new ArrayList<>();
    private final List<NormalParameter> args = new ArrayList<>();
    private final CommandDefine define;
    private final Command command;
    private final String mapping;
    private final int mappingCount;
    private final long cooldown;
    private final boolean async;
    private final String cooldownExecludePermission;
    private final TabComplete tabComplete;
    private final CommandConcurrentLevelEnum concurrentLevel;
    @Autowired
    private CacheService cacheService;
    @Autowired
    private PluginUtils pluginUtils;
    /**
     * 正在执行的指令
     */
    private Set<String> senderExecuting;
    private String allExecuting;

    public CommandExecutor(@NonNull Object target, @NonNull Method invoker) {
        this.target = target;
        invoker.setAccessible(true);
        this.invoker = invoker;
        this.define = ClassUtils.getUserClass(target).getAnnotation(CommandDefine.class);
        Assert.notNull(define, String.format("illegal command definition! target=%s, method=%s", String.valueOf(target), String.valueOf(invoker)));
        Assert.isTrue(!Modifier.isStatic(invoker.getModifiers()), String.format("illegal command definition! target=%s, method=%s", String.valueOf(target), String.valueOf(invoker)));
        name = define.name();
        if (!define.permission().isEmpty()) {
            permissions.add(define.permission());
        }
        this.command = invoker.getAnnotation(Command.class);
        cooldown = this.command.cooldown() < 0 ? 0 : this.command.cooldown() * 1000000;
        cooldownExecludePermission = this.command.cooldownExecludePermission();
        showArgs = command.showArgs();
        description = command.des();
        async = command.async();
        concurrentLevel = command.concurrentLevel();
        if (concurrentLevel == CommandConcurrentLevelEnum.SENDER) {
            senderExecuting = Collections.newSetFromMap(new ConcurrentHashMap<>());
        }
        Assert.notNull(command, String.format("illegal command definition! target=%s, method=%s", String.valueOf(target), String.valueOf(invoker)));
        if (!command.permission().isEmpty()) {
            permissions.add(command.permission());
        }

        Parameter[] parameters = invoker.getParameters();
        int ignoreIndex = 0;
        for (int i = 0; i < parameters.length; i++) {
            ignoreIndex += resolveParameter(i, ignoreIndex, parameters[i]);
        }
        String mapping = define.mapping() + " " + command.mapping() + " " + (command.useMethodNameAsFirtMapping() ? invoker.getName() + " " : "");
        mapping = mapping.replaceAll(" +", " ");
        this.mapping = mapping.toLowerCase().trim();
        mappingCount = this.mapping.split(" ").length;
//      tab complete
        if (this.command.tabComplete() == TabComplete.class) {
            this.tabComplete = null;
        } else {
            this.tabComplete = SpringContext.getBean(this.command.tabComplete());
        }
    }


    public boolean execute(CommandSender commandSender, CommandContext context) {
        if (!hasPermisson(commandSender, true)) {
            return false;
        }
        if (!checkCooldown(commandSender)) {
            return false;
        }
        if (!checkConcurrent(commandSender)) {
            return false;
        }
        Object[] parameters = resolveParameter(commandSender, context);
        if (async) {
            asyncExecute(commandSender, context, parameters);
            return true;
        } else {
            Object r = syncExecute(commandSender, context, parameters);
            boolean executed = r instanceof Boolean ? (Boolean) r : true;
            return executed;
        }
    }

    private Object[] resolveParameter(CommandSender commandSender, CommandContext context) {
        try {
            int parameterCount = senders.size() + flags.size() + flagMap.size() + this.args.size();
            Object[] args = new Object[parameterCount];
            for (Integer index : senders) {
                args[index] = commandSender;
            }
            for (FlagParameter flag : flags) {
                args[flag.getIndex()] = flag.resolve(commandSender, context);
            }
            for (Integer index : flagMap) {
                args[index] = context.getFalgMap();
            }
            for (NormalParameter arg : this.args) {
                args[arg.getIndex()] = arg.resolve(commandSender, context, mappingCount);
                if (command.resolveColorCode() && args[arg.getIndex()] instanceof String) {
                    args[arg.getIndex()] = ChatColor.translateAlternateColorCodes('&', (String) args[arg.getIndex()]);
                }
            }
            return args;
        } catch (NumberFormatException e) {
            throw new CommandException("数字格式错误:" + e.getMessage());
        } catch (Throwable e) {
            help(commandSender);
            ExceptionUtils.throwException(e);
            return null;
        } finally {
            //            标记执行完毕
            markExecuteEnd(commandSender);
        }
    }

    private Object syncExecute(CommandSender commandSender, CommandContext context, Object[] args) {
        try {
            return invoker.invoke(target, args);
        } catch (Throwable e) {
            ExceptionUtils.throwException(e);
            throw new UnreachableException();
        } finally {
//            标记执行完毕
            markExecuteEnd(commandSender);
        }
    }

    private void asyncExecute(CommandSender commandSender, CommandContext context, Object[] args) {
        //                异步执行指令
        Bukkit.getScheduler().runTaskAsynchronously(pluginUtils.getSelf(), () -> {
            try {
                invoker.invoke(target, args);
            } catch (Throwable e) {
                CommandAdapter.resolveException(commandSender, e, this.name + " " + CommandAdapter.joiner.join(context.getOrigin()));
            } finally {
                //            标记执行完毕
                markExecuteEnd(commandSender);
            }
        });
    }

    private void markExecuteEnd(CommandSender commandSender) {
        if (concurrentLevel == CommandConcurrentLevelEnum.NONE) {

        } else if (concurrentLevel == CommandConcurrentLevelEnum.ALL) {
            allExecuting = null;
        } else {
            this.senderExecuting.remove(commandSender.getName());
        }
    }

    private void markExecuteStart(CommandSender commandSender) {
        if (concurrentLevel == CommandConcurrentLevelEnum.NONE) {

        } else if (concurrentLevel == CommandConcurrentLevelEnum.ALL) {
            allExecuting = commandSender.getName();
        } else {
            this.senderExecuting.add(commandSender.getName());
        }
    }

    private boolean hasPermisson(CommandSender commandSender, boolean notify) {
        if (isOnlyConsole() && !(commandSender instanceof ConsoleCommandSender)) {
            if (notify) {
                commandSender.sendMessage(ChatColor.RED.toString() + "权限不足,需要以控制台权限执行!");
            }
            return false;
        }
        if (isOnlyPlayer() && !(commandSender instanceof Player)) {
            if (notify) {
                commandSender.sendMessage(ChatColor.RED.toString() + "权限不足,需要以玩家身份执行!");
            }
            return false;
        }
        if (isOnlyOp() && !commandSender.isOp()) {
            if (notify) {
                commandSender.sendMessage(ChatColor.RED.toString() + "权限不足,需要以op身份执行!");
            }
            return false;
        }
        for (String permission : permissions) {
            if (!commandSender.hasPermission(permission)) {
                if (notify) {
                    commandSender.sendMessage(ChatColor.RED.toString() + "权限不足:" + permission);
                }
                return false;
            }
        }
        return true;
    }

    public int help(CommandSender commandSender, List<RawMessage> helps) {
        if (!hasPermisson(commandSender, false)) {
            return 0;
        }
        RawMessage raw = getHelpMessage();
        helps.add(raw);
        return 1;
    }

    public String getDescription() {
        return SpringContext.getApplicationEnvironment().resolvePlaceholders(description);
    }

    public RawMessage getHelpMessage() {
        return RawMessage.createRawMessage(("§6/" + this.getName() + " §b" + getShowArgs()).trim())
                .suggestCommand("/" + this.getName() + " " + getShowArgs())
                .then(" §6-§a " + getDescription());
    }

    public int help(CommandSender commandSender) {
        if (!hasPermisson(commandSender, false)) {
            return 0;
        }
        getHelpMessage().send(commandSender);
        return 1;
    }

    public void forceHelp(CommandSender commandSender) {
        getHelpMessage().send(commandSender);
    }


    public boolean isOnlyConsole() {
        return command.onlyConsole();
    }

    public boolean isOnlyPlayer() {
        return command.onlyPlayer();
    }

    public boolean isOnlyOp() {
        return command.onlyOp();
    }

    private int resolveParameter(int index, int ignoreIndex, Parameter parameter) {
        {
            if (parameter.isAnnotationPresent(Sender.class)) {
                if (!CommandSender.class.isAssignableFrom(parameter.getType())) {
                    throw new CommandException("@Sender only allows annotations on the CommandSender or its implements");
                }
                if (isOnlyPlayer() && !Player.class.isAssignableFrom(parameter.getType())) {
                    throw new CommandException("This method only allows player to exectue but the parameter is " + parameter.getType().getSimpleName());
                }
                if (isOnlyConsole() && !ConsoleCommandSender.class.isAssignableFrom(parameter.getType())) {
                    throw new CommandException("This method only allows console to exectue but the parameter is " + parameter.getType().getSimpleName());
                }
                senders.add(index);
                return 1;
            }
        }
        boolean required = !parameter.isAnnotationPresent(Optional.class);
        {
            Flag flag = parameter.getAnnotation(Flag.class);
            if (flag != null) {
                CommandResolver resolver;
                if (parameter.getParameterizedType() == boolean.class || parameter.getParameterizedType() == Boolean.class) {
                    resolver = CommandResolvers.getBooleanFlagResolver();
                } else {
                    resolver = CommandResolvers.getResolver(parameter.getParameterizedType());
                }
                Assert.notNull(resolver, String.format("unsupported parameter type:%s", parameter.getParameterizedType().toString()));
                flags.add(FlagParameter.builder()
                        .index(index)
                        .flag(flag.value())
                        .resolver(resolver)
                        .required(required)
                        .type(parameter.getParameterizedType())
                        .build());
                return 1;
            }
        }
        {
            if (parameter.isAnnotationPresent(FlagMap.class)) {
                flagMap.add(index);
                return 1;
            }
        }
        {
            CommandResolver resolver = CommandResolvers.getResolver(parameter.getParameterizedType());
            Assert.notNull(resolver, String.format("unsupported parameter type:%s", parameter.getParameterizedType().toString()));
            args.add(NormalParameter.builder()
                    .index(index)
                    .resolver(resolver)
                    .ignoreIndex(ignoreIndex)
                    .required(required)
                    .type(parameter.getParameterizedType())
                    .build());
            return 0;
        }
    }

    private boolean checkCooldown(CommandSender commandSender) {
        long cooldown = getCooldown(commandSender);
        if (cooldown >= 3000) {
            commandSender.sendMessage(ChatColor.RED + "指令冷却中,剩余" + (cooldown / 1000) + "秒");
            return false;
        }
        if (cooldown > 0) {
            commandSender.sendMessage(ChatColor.RED + "指令冷却中,剩余" + (cooldown) + "毫秒");
            return false;
        }
//      标记此指令执行了,记入冷却系统
        markExecuted(commandSender);
        return true;
    }

    private boolean checkConcurrent(CommandSender commandSender) {
        if (concurrentLevel == CommandConcurrentLevelEnum.NONE) {
            return true;
        } else if (concurrentLevel == CommandConcurrentLevelEnum.ALL) {
            if (allExecuting != null) {
                commandSender.sendMessage(ChatColor.RED + "此指令只能同时一个玩家执行,现在  " + allExecuting + " 正在执行,请稍后再试!");
                return false;
            }
            markExecuteStart(commandSender);
            return true;
        } else {
            if (this.senderExecuting.contains(commandSender.getName())) {
                commandSender.sendMessage(ChatColor.RED + "您已有一条指令正在执行,请稍后再试");
                return false;
            }
            markExecuteStart(commandSender);
            return true;
        }
    }

    private void markExecuted(CommandSender commandSender) {
        if (cooldown <= 0) {
            return;
        }
        if (cooldownExecludePermission != null && !cooldownExecludePermission.isEmpty() && commandSender.hasPermission(cooldownExecludePermission)) {
//            有特权,不受冷却限制
            return;
        }
        String key = getCooldownKey(commandSender);
        CacheItem<Object> cacheItem = new CacheItem();
        cacheItem.setExpireAfterCreate(cooldown);
        cacheService.putCacheItem(key, cacheItem);
    }

    private long getCooldown(CommandSender commandSender) {
        if (cooldown <= 0 || commandSender.isOp()) {
            return 0;
        }
        String key = getCooldownKey(commandSender);
        CacheItem<Object> item = cacheService.getCacheItem(key);
        if (item == null) {
            return 0;
        }
        return item.getLifeTime() / 1000000L;
    }

    private String getCooldownKey(CommandSender commandSender) {
        return "sbp.command.cooldown." + this.mapping + "." + commandSender.getName();
    }

}
